/**
This module includes a lot of documentation 
which would help a developer new to lighttpd

A lot of the documentation have been taken from
http://trac.lighttpd.net/trac/wiki/HowToWriteALighttpdPlugin
**/
#include <sys/stat.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>

#include "base.h"
#include "log.h"
#include "buffer.h"
#include "http_chunk.h"
#include "etag.h"

#include "plugin.h"

#include "response.h"
#include "stat_cache.h"
#include "stream.h"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_SYS_SYSLIMITS_H
#include <sys/syslimits.h>
#endif

#ifdef HAVE_ATTR_ATTRIBUTES_H
#include <attr/attributes.h>
#endif

/**
Every plugin can opt-in to handle conditionals from the global configfile and
store the different settings in a by-condition plugin_config. 
This struct is linked in the next plugin-wide struct: plugin_data
**/
/* plugin config for all request/connections */
typedef struct {
	array *include_ext;
} plugin_config;


/**
Each plugin has some local data which only belongs to the plugin 
(like the config and some temporary buffers). 
**/
typedef struct {
	PLUGIN_DATA;

	buffer *match_buf;

	plugin_config **config_storage;

	plugin_config conf;
} plugin_data;


/**
If a plugin has to store connection specific information, 
you need another structure with one entry per connection. 
Take a look at mod_rewrite as an example. 
**/
typedef struct {
	size_t foo;
} handler_ctx;

static handler_ctx * handler_ctx_init() {
	handler_ctx * hctx;

	hctx = calloc(1, sizeof(*hctx));

	return hctx;
}

static void handler_ctx_free(handler_ctx *hctx) {

	free(hctx);
}

/**
The *_init function is called to initialize the plugin itself and 
returns the plugin_data struct with some usefull defaults. 
**/
/* init the plugin data */
INIT_FUNC(mod_concat_init) {
	plugin_data *p;
	
	p = calloc(1, sizeof(*p));

	p->match_buf = buffer_init();

	return p;
}

/**
*_free is called at the end of the life of a plugin and 
is used to tell the plugin to release all allocated memory. 
Keep in mind that it is preferred not to let the program termination 
clean up memory for you. Free what ever you have malloced. 
Use valgrind or something else to verify your work. 
**/
/* detroy the plugin data */
FREE_FUNC(mod_concat_free) {
	plugin_data *p = p_d;

	UNUSED(srv);

	if (!p) return HANDLER_GO_ON;

	if (p->config_storage) {
		size_t i;

		for (i = 0; i < srv->config_context->used; i++) {
			plugin_config *s = p->config_storage[i];

			if (!s) continue;

			array_free(s->include_ext);

			free(s);
		}
		free(p->config_storage);
	}

	buffer_free(p->match_buf);

	free(p);

	return HANDLER_GO_ON;
}

/**
As soon as the configfile is parsed, each plugin gets the chance
to fetch its config-values from the config and verify it internally. 
A config_values_t struct contains the config-key and the type you 
expect. Don't forget that the last entry of the struct should be 
NULL to mark the end.
**/
/* handle plugin config and check values */
SETDEFAULTS_FUNC(mod_concat_set_defaults) {
	plugin_data *p = p_d;
	size_t i = 0;

	config_values_t cv[] = {
		{ "concat.include_ext",  NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION },       /* 0 */
		{ NULL,                  NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
	};

	if (!p) return HANDLER_ERROR;

	p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *));

	for (i = 0; i < srv->config_context->used; i++) {
		plugin_config *s;

		s = calloc(1, sizeof(plugin_config));
		s->include_ext    = array_init();

		cv[0].destination = s->include_ext;

		p->config_storage[i] = s;

		if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) {
			return HANDLER_ERROR;
		}
	}

	return HANDLER_GO_ON;
}


/**
really simple macro which sets the the value of the plugin_data struct
equal to the value in the plugin_config storage.
**/
#define PATCH(x) \
	p->conf.x = s->x;
static int mod_concat_patch_connection(server *srv, connection *con, plugin_data *p) {
	size_t i, j;
	plugin_config *s = p->config_storage[0];

	PATCH(include_ext);

	/* skip the first, the global context */
	for (i = 1; i < srv->config_context->used; i++) {
		data_config *dc = (data_config *)srv->config_context->data[i];
		s = p->config_storage[i];

		/* condition didn't match */
		if (!config_check_cond(srv, con, dc)) continue;

		/* merge config */
		for (j = 0; j < dc->value->used; j++) {
			data_unset *du = dc->value->data[j];
			
			if (buffer_is_equal_string(du->key, CONST_STR_LEN("concat.include_ext"))) {
				PATCH(include_ext);
			}
		}
	}

	return 0;
}
#undef PATCH

/**
Utility function: checks that the file_name has an extension which 
is defined in the config file
**/
int allowed_file_ext(plugin_data *p, buffer * file_name) {
    size_t k;
    for (k = 0; k < p->conf.include_ext->used; k++) {
        data_string *ds = (data_string *)p->conf.include_ext->data[k];        
        if (ds->value->used == 0) continue;
        if(buffer_is_equal_right_len(file_name, ds->value, ds->value->used - 1)) {
            return 1;
        }
    }
    return 0;
}


/**
Directly taken from etag.c,
We pass in the modified time and size rather than a stat file struct.
We ignore the ETAG_USE_INODE flags
**/
int concat_etag_create(buffer *etag, time_t mtime, size_t size, etag_flags_t flags) {
	if (0 == flags) return 0;

	buffer_reset(etag);
	
	if (flags & ETAG_USE_SIZE) {
		buffer_append_off_t(etag, size);
		buffer_append_string_len(etag, CONST_STR_LEN("-"));
	}
	
	if (flags & ETAG_USE_MTIME) {
		buffer_append_long(etag, mtime);
	}

	return 0;
}


/***
This is where most of the work is done;

1. takes the requested path;
2. splits the query string into files (which are delimited by comma)
3. ensures that the file can be loaded by the plugin;
    1. The file exists
    2. The file_name doesn't start with . (for hidden files and relative directory paths)
    3. the requested file has an file extension defined in the configure file
4. add the file content to the response
**/
static int http_concat_files(server *srv, connection *con, plugin_data *p, buffer *dir) {
    char *token;
    int count = 0;
    stat_cache_entry *sce = NULL;
    time_t modified_time;
    size_t response_size;
    buffer *mtime;
    buffer *content_type = buffer_init();
    data_string *ds;
    char * fl = con->uri.query->ptr;
    
    /** remove the first character ? **/
    if(fl[0] == '?') fl++;

    /** 
    The query string is a comma seperated string of filenames.
    We split them here and iterate over each
    **/
    token = strtok(fl, ",");
    
    while(token != NULL) {
        /** ignore dot files for obvious reasons **/
        if(token[0] == '.') {
            log_error_write(srv, __FILE__, __LINE__,  "ss", 
                            "hidden file ignored:", token);
        } else {
            buffer *file_name = buffer_init();
        	buffer_copy_string(file_name, token);
        	
            /** check file extension is in the whitelist **/
            if(allowed_file_ext(p, file_name)) {
                count++;
                log_error_write(srv, __FILE__, __LINE__,  "sb", 
                                "added file:", file_name);
                
                buffer *file_location = buffer_init();
                buffer_copy_string_buffer(file_location, dir);
            	/** this won't work on windows servers. **/
            	buffer_append_string(file_location, token);

                /** open file **/                                                
                if (HANDLER_ERROR == stat_cache_get_entry(srv, con, file_location, &sce)) {
                    log_error_write(srv, __FILE__, __LINE__, "sbsb",
                                    "not a regular file:", con->uri.path, "->", file_name);
                } else {
                    /** set the length of the response **/
                    response_size += sce->st.st_size;

                    /** content type from the first file **/
                    if (count == 1) {
                        if(!buffer_is_empty(sce->content_type)) {
                            buffer_copy_string_buffer(content_type, sce->content_type);
                        }
                    }
                    
                    /**
                    We set the modified time to the time 
                    of the latest modified file
                    **/ 
                    if (modified_time < sce->st.st_mtime) {
                        modified_time = sce->st.st_mtime;
                    }
                    
                    /**
                    Append a new line between every file
                    **/
                    if (count != 1){
                        http_chunk_append_mem(srv, con, "\n\r", 2);
                    }
                    /** 
                    Append the the content of the file the the response
                    **/
                    http_chunk_append_file(srv, con, file_location, 0, sce->st.st_size);
                }
            } else {
                log_error_write(srv, __FILE__, __LINE__,  "ss", "concat ignored file:", token);
            }
        }
        token = strtok(NULL, ",");
    }
    
    if(count == 0) {
        return -1;
    }
    
    /**
    We create a ETag based on the modified time and the size of the response. 
    **/
     if (NULL == array_get_element(con->response.headers, "ETag")) {
         buffer *b = buffer_init();
         concat_etag_create(b, mtime, response_size, con->etag_flags);
         response_header_overwrite(srv, con, CONST_STR_LEN("ETag"), CONST_BUF_LEN(b));
     }
    
    mtime = strftime_cache_get(srv, modified_time);
    
    response_header_overwrite(srv, con, CONST_STR_LEN("Content-Type"), CONST_BUF_LEN(content_type));
    response_header_overwrite(srv, con, CONST_STR_LEN("Last-Modified"), CONST_BUF_LEN(mtime));
    con->http_status = 200;
    con->file_finished = 1;
    return 0;
}



/**
URIHANDLER_FUNC is a macro which simplifies handler function definition
**/
URIHANDLER_FUNC(mod_concat_uri_handler) {
	plugin_data *p = p_d; 
	stat_cache_entry *sce = NULL;
	
	UNUSED(srv);
    
    /**
    We want to find out if this request should be handled by us as
    soon as possible, so we can let the request continue down the 
    list of lighttpd modules.
    **/
    
    /** Path set? **/
    if (con->uri.path->used == 0) return HANDLER_GO_ON;
    /** Query set? **/
    if (con->uri.query->used == 0) return HANDLER_GO_ON;
    /** Full path (ends with '/' for directory lookup) **/
    if (con->uri.path->ptr[con->uri.path->used - 2] != '/') 
        return HANDLER_GO_ON;
    /* Ensure the HTTP method is GET */
    if(con->request.http_method != HTTP_METHOD_GET) 
        return HANDLER_GO_ON;
    /** 
    Special Case: Our syntax requires a '?' 
    as the first character in the query string 
    **/
    if (con->uri.query->ptr[0] != '?') return HANDLER_GO_ON;

    /**
    After we know the request is for us to handle,
    we patch the connection with the required settings
    for the module
    **/
    mod_concat_patch_connection(srv, con, p);

    /**
    If request logging is enabled
    output that we are handling the request
    and relevant request details
    **/
    if (con->conf.log_request_handling) {
            log_error_write(srv, __FILE__, __LINE__,  "s",  
                             "-- handling the request as concat");
            log_error_write(srv, __FILE__, __LINE__,  "sb", 
                            "uri-path   :", con->uri.path);
            log_error_write(srv, __FILE__, __LINE__,  "sb", 
                            "uri-query  :", con->uri.query);
        }
    
    
    /**
    Another check before we start to handle the request, ensure, 
    we have access to the requested directory
    **/
    if (HANDLER_ERROR == stat_cache_get_entry(srv, con, con->physical.path, &sce)) {
		fprintf(stderr, "%s.%d: %s\n", __FILE__, __LINE__, con->physical.path->ptr);
        SEGFAULT();
    }
    
    /**
    Yet another check, we ensure that the path is a directory, not a file
    **/
    if (!S_ISDIR(sce->st.st_mode)) return HANDLER_GO_ON;

    /**
    We now process the request in the http_concat_files function
    **/
    if (http_concat_files(srv, con, p, con->physical.path)) {
        /** 
        Something went wrong in the http_concat_files function.
        So we set the http_status accordingly
        **/
        con->http_status = 403;
    }
	
	/**
	Clear the buffer before finishing
	**/
	buffer_reset(con->physical.path);
	
	/**
	We have completed our job, tell lighttpd accordingly
	**/
	return HANDLER_FINISHED;
}

/**
Each plugin has to have a ..._plugin_init function which is 
called when the plugin is called. It should only set p->name
 to a buffer containing the name of the plugin (human readable) 
and should fill the hooks (init, set_defaults, cleanup) and 
return with 0; 
**/
/* this function is called at dlopen() time and inits the callbacks */
int mod_concat_plugin_init(plugin *p) {
    
    /** Plugin Version **/
	p->version     = LIGHTTPD_VERSION_ID;
	
	/** Plugin Name **/
	p->name        = buffer_init_string("concat");
	
	/** 
	The plugin constructor
	**/
	p->init        = mod_concat_init;
	
	/** 
	lighttpd defines a number of entry points which a 
	plugin can handle a request. For this plugin we require
	handle_subrequest_start but other possible alternatives are
	**/
    p->handle_subrequest_start  = mod_concat_uri_handler;

    /**
    The default configuration settings
    **/
	p->set_defaults  = mod_concat_set_defaults;

	/**
	The plugin deconstructor 
	**/
	p->cleanup     = mod_concat_free;
	p->data        = NULL;
	
	return 0;
}
